iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0

為了下一篇方便示範 Raymarching,這邊先來導入 fbx 與 mesh、material 相關的程式。這裡使用 assimp 來處理內容的讀取。

實作

//mesh.h

#pragma once
#include <memory>
#include <string>
#include <vector>

#include <DirectXMath.h>

#include "index_buffer.h"
#include "vertex_buffer.h"

class RenderContext;

struct MeshVertex
{
    DirectX::XMFLOAT3 position;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT2 uv;
};

struct SMesh
{
    VertexBuffer m_vertexBuffer;
    std::vector<IndexBuffer> m_indexBuffers;
};

class Mesh
{
  public:
    void initFromFbxFile(const std::string& path);
    void draw(RenderContext& renderContext) const;

  private:
    std::vector<std::unique_ptr<SMesh>> m_meshes;
};
//mesh.cpp

#include "mesh.h"

#include <cstdint>
#include <limits>
#include <stdexcept>

#include <assimp/Importer.hpp>
#include <assimp/postprocess.h>
#include <assimp/scene.h>

#include "render_context.h"

namespace
{
// 檢查 Buffer 元素數量是否能安全轉換成 int
int checkedBufferCount(std::size_t count, const char* bufferName)
{
    // 確認元素數量沒有超過 int 可表示的最大值
    if (count > static_cast<std::size_t>(std::numeric_limits<int>::max()))
    {
        // 超過支援範圍時拋出例外並指出是哪一種 Buffer
        throw std::runtime_error(std::string("Mesh: ") + bufferName + " exceeds the supported size.");
    }

    // 將確認安全的元素數量轉換成 int
    return static_cast<int>(count);
}
} // namespace

// 從指定的 FBX 檔案載入 Mesh 資料並建立 GPU Buffer
void Mesh::initFromFbxFile(const std::string& path)
{
    // 檢查 FBX 檔案路徑是否為空
    if (path.empty())
    {
        // 路徑為空時拋出參數錯誤
        throw std::invalid_argument("Mesh: FBX path must not be empty.");
    }

    // 建立 Assimp Importer 負責讀取模型檔案
    Assimp::Importer importer;

    // 讀取 FBX 並進行三角化、轉換成 Left-Handed、產生法線與合併重複頂點
    const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_ConvertToLeftHanded |
                                                       aiProcess_GenNormals | aiProcess_JoinIdenticalVertices);

    // 檢查模型是否載入失敗
    if (scene == nullptr)
    {
        // 將 Assimp 提供的錯誤訊息一起拋出
        throw std::runtime_error("Mesh: Failed to load FBX file '" + path + "': " + importer.GetErrorString());
    }

    // 檢查場景內是否至少存在一個 Mesh
    if (!scene->HasMeshes())
    {
        throw std::runtime_error("Mesh: FBX file contains no meshes: " + path);
    }

    // 暫存這次成功載入的所有 Mesh
    std::vector<std::unique_ptr<SMesh>> loadedMeshes;

    // 預先配置足夠空間避免 vector 重複重新配置
    loadedMeshes.reserve(scene->mNumMeshes);

    // 逐一處理 Assimp 場景中的 Mesh
    for (unsigned int meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
    {
        // 取得目前 Mesh 的參考
        const aiMesh& sourceMesh = *scene->mMeshes[meshIndex];

        // 確認 Mesh 同時具有頂點位置與法線資料
        if (!sourceMesh.HasPositions() || !sourceMesh.HasNormals())
        {
            throw std::runtime_error("Mesh: FBX mesh " + std::to_string(meshIndex) +
                                     " is missing required position or normal data.");
        }

        // 建立用來存放轉換後頂點資料的陣列
        std::vector<MeshVertex> vertices;

        // 預先配置與來源頂點數量相同的空間
        vertices.reserve(sourceMesh.mNumVertices);

        // 逐一轉換來源 Mesh 的頂點資料
        for (unsigned int vertexIndex = 0; vertexIndex < sourceMesh.mNumVertices; ++vertexIndex)
        {
            // 取得目前頂點的位置
            const aiVector3D& position = sourceMesh.mVertices[vertexIndex];

            // 取得目前頂點的法線
            const aiVector3D& normal = sourceMesh.mNormals[vertexIndex];

            // 若存在第 0 組 UV 則讀取,否則使用零向量
            const aiVector3D uv =
                sourceMesh.HasTextureCoords(0) ? sourceMesh.mTextureCoords[0][vertexIndex] : aiVector3D{};

            // 將位置、法線與 UV 轉換成引擎使用的 MeshVertex 格式
            vertices.push_back({{position.x, position.y, position.z}, {normal.x, normal.y, normal.z}, {uv.x, uv.y}});
        }

        // 建立 32-bit Index 陣列
        std::vector<std::uint32_t> indices;

        // 每個三角形需要三個 Index,因此預先配置 Face 數量乘以三
        indices.reserve(static_cast<std::size_t>(sourceMesh.mNumFaces) * 3);

        // 逐一處理 Mesh 中的 Face
        for (unsigned int faceIndex = 0; faceIndex < sourceMesh.mNumFaces; ++faceIndex)
        {
            // 取得目前 Face
            const aiFace& face = sourceMesh.mFaces[faceIndex];

            // 確認三角化後的 Face 確實只有三個 Index
            if (face.mNumIndices != 3)
            {
                throw std::runtime_error("Mesh: FBX mesh " + std::to_string(meshIndex) +
                                         " contains a non-triangle face after triangulation.");
            }

            // 將目前三角形的所有 Index 加入 Index 陣列
            indices.insert(indices.end(), face.mIndices, face.mIndices + face.mNumIndices);
        }

        // 確認 Mesh 具有可供繪製的頂點與 Index
        if (vertices.empty() || indices.empty())
        {
            throw std::runtime_error("Mesh: FBX mesh " + std::to_string(meshIndex) +
                                     " contains no renderable geometry.");
        }

        // 建立新的 SMesh 並由 unique_ptr 管理生命週期
        auto mesh = std::make_unique<SMesh>();

        // 建立 Vertex Buffer 並設定頂點數量與單一頂點大小
        mesh->m_vertexBuffer.init(checkedBufferCount(vertices.size(), "vertex count"),
                                  static_cast<int>(sizeof(MeshVertex)));

        // 將頂點資料複製到 Vertex Buffer
        mesh->m_vertexBuffer.copy(vertices.data());

        // 新增一個 Index Buffer
        mesh->m_indexBuffers.emplace_back();

        // 取得剛新增的 Index Buffer
        IndexBuffer& indexBuffer = mesh->m_indexBuffers.back();

        // 建立 Index Buffer 並設定 Index 數量與單一 Index 大小
        indexBuffer.init(checkedBufferCount(indices.size(), "index count"), static_cast<int>(sizeof(std::uint32_t)));

        // 將 Index 資料複製到 Index Buffer
        indexBuffer.copy(indices.data());

        // 將完成初始化的 Mesh 移入暫存陣列
        loadedMeshes.push_back(std::move(mesh));
    }

    // 所有 Mesh 都成功載入後再一次替換目前持有的 Mesh
    m_meshes = std::move(loadedMeshes);
}

// 繪製目前 Mesh 物件持有的所有幾何資料
void Mesh::draw(RenderContext& renderContext) const
{
    // 逐一處理所有子 Mesh
    for (const auto& mesh : m_meshes)
    {
        // 將目前 Mesh 的 Vertex Buffer 綁定到 Render Pipeline
        renderContext.setVertexBuffer(mesh->m_vertexBuffer);

        // 逐一處理目前 Mesh 的所有 Index Buffer
        for (const IndexBuffer& indexBuffer : mesh->m_indexBuffers)
        {
            // 綁定目前的 Index Buffer
            renderContext.setIndexBuffer(indexBuffer);

            // 根據 Index 數量送出 Indexed Draw Call
            renderContext.drawIndexed(indexBuffer.getCount());
        }
    }
}
//material.h

#pragma once

#include <DirectXMath.h>

#include "constant_buffer.h"

struct MaterialConstants
{
    DirectX::XMFLOAT4 baseColor = {1.0f, 1.0f, 1.0f, 1.0f};
    DirectX::XMFLOAT4 emissiveAndStrength = {0.0f, 0.0f, 0.0f, 0.0f};
    DirectX::XMFLOAT4 surface = {0.5f, 0.0f, 1.0f, 0.0f};
};

static_assert(sizeof(MaterialConstants) % 16 == 0);

class Material
{
  public:
    void init(const MaterialConstants& constants = {});
    void setConstants(const MaterialConstants& constants);
    D3D12_GPU_VIRTUAL_ADDRESS getConstantsAddress() const;

  private:
    ConstantBuffer m_constantBuffer;
    MaterialConstants m_constants{};
};

//material.cpp

#include "material.h"

// 使用材質常數初始化 Material 與對應的 Constant Buffer
void Material::init(const MaterialConstants& constants)
{
    // 保存目前材質使用的常數資料
    m_constants = constants;

    // 建立 Constant Buffer 並寫入初始材質常數
    m_constantBuffer.init(sizeof(MaterialConstants), &m_constants);
}

// 更新材質常數並同步到 GPU
void Material::setConstants(const MaterialConstants& constants)
{
    // 更新 CPU 端保存的材質常數
    m_constants = constants;

    // 將最新的材質常數複製到 VRAM
    m_constantBuffer.copyToVRAM(m_constants);
}

// 取得材質 Constant Buffer 的 GPU Virtual Address
D3D12_GPU_VIRTUAL_ADDRESS Material::getConstantsAddress() const
{
    // 回傳 Constant Buffer 在 GPU 上的虛擬位址
    return m_constantBuffer.getGPUVirtualAddress();
}
///main.cpp

#include <cstdlib>
#include <cstring>
#include <exception>
#include <filesystem>
#include <stdexcept>
#include <string>

#include <DirectXMath.h>
#include <directx/d3dx12_core.h>
#include <wrl/client.h>

#include "graphics_engine.h"
#include "mesh.h"
#include "my_engine.h"
#include "pipeline_state.h"
#include "render_context.h"
#include "shader.h"
#include "system.h"

namespace
{
using Microsoft::WRL::ComPtr;

struct TransformConstants
{
    DirectX::XMFLOAT4X4 worldViewProjection;
};

// 取得執行檔所在目錄,方便從相對路徑尋找 FBX 資源
std::filesystem::path getExecutableDirectory()
{
    wchar_t executablePath[MAX_PATH]{};
    const DWORD pathLength = GetModuleFileNameW(nullptr, executablePath, _countof(executablePath));
    if (pathLength == 0 || pathLength == _countof(executablePath))
        throw std::runtime_error("DayX_ReadFBX: Failed to determine the executable directory.");

    return std::filesystem::path(executablePath).parent_path();
}

ComPtr<ID3D12RootSignature> createRootSignature(ID3D12Device* device)
{
    if (device == nullptr)
        throw std::invalid_argument("DayX_ReadFBX: A graphics device is required.");

    D3D12_ROOT_PARAMETER transformParameter{};
    transformParameter.ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;
    transformParameter.Descriptor.ShaderRegister = 0;
    transformParameter.Descriptor.RegisterSpace = 0;
    transformParameter.ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;

    D3D12_ROOT_SIGNATURE_DESC description{};
    description.NumParameters = 1;
    description.pParameters = &transformParameter;
    description.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

    ComPtr<ID3DBlob> serializedRootSignature;
    ComPtr<ID3DBlob> errorBlob;
    const HRESULT serializeResult = D3D12SerializeRootSignature(
        &description, D3D_ROOT_SIGNATURE_VERSION_1, serializedRootSignature.GetAddressOf(), errorBlob.GetAddressOf());
    if (FAILED(serializeResult))
    {
        std::string message = "DayX_ReadFBX: Failed to serialize the root signature.";
        if (errorBlob != nullptr)
        {
            message += "\n";
            message.append(static_cast<const char*>(errorBlob->GetBufferPointer()), errorBlob->GetBufferSize());
        }
        throw std::runtime_error(message);
    }

    ComPtr<ID3D12RootSignature> rootSignature;
    if (FAILED(device->CreateRootSignature(0, serializedRootSignature->GetBufferPointer(),
                                           serializedRootSignature->GetBufferSize(),
                                           IID_PPV_ARGS(rootSignature.GetAddressOf()))))
    {
        throw std::runtime_error("DayX_ReadFBX: Failed to create the root signature.");
    }
    return rootSignature;
}

PipelineState createPipelineState(ID3D12RootSignature* rootSignature, Shader& vertexShader, Shader& pixelShader)
{
    if (rootSignature == nullptr)
        throw std::invalid_argument("DayX_ReadFBX: A root signature is required.");

    // 定義 FBX 頂點資料中的 Position、Normal 與 UV Layout
    static constexpr D3D12_INPUT_ELEMENT_DESC inputElementDescriptions[] = {
        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
        {"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
        {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
    };

    D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};
    description.InputLayout = {inputElementDescriptions, _countof(inputElementDescriptions)};
    description.pRootSignature = rootSignature;
    description.VS = CD3DX12_SHADER_BYTECODE(vertexShader.getCompiledBlob());
    description.PS = CD3DX12_SHADER_BYTECODE(pixelShader.getCompiledBlob());
    description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
    description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
    description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    description.DepthStencilState.DepthEnable = FALSE;
    description.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
    description.DepthStencilState.StencilEnable = FALSE;
    description.SampleMask = UINT_MAX;
    description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    description.NumRenderTargets = 1;
    description.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    description.SampleDesc.Count = 1;

    PipelineState pipelineState;
    pipelineState.init(description);
    return pipelineState;
}

ComPtr<ID3D12Resource> createTransformBuffer(ID3D12Device* device, const TransformConstants& constants)
{
    if (device == nullptr)
        throw std::invalid_argument("DayX_ReadFBX: A graphics device is required.");

    constexpr UINT64 allocationSize = D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT;
    const D3D12_HEAP_PROPERTIES heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
    const D3D12_RESOURCE_DESC resourceDescription = CD3DX12_RESOURCE_DESC::Buffer(allocationSize);

    ComPtr<ID3D12Resource> transformBuffer;
    if (FAILED(device->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE, &resourceDescription,
                                               D3D12_RESOURCE_STATE_GENERIC_READ, nullptr,
                                               IID_PPV_ARGS(transformBuffer.GetAddressOf()))))
    {
        throw std::runtime_error("DayX_ReadFBX: Failed to create the transform constant buffer.");
    }

    void* mappedData = nullptr;
    const D3D12_RANGE readRange{0, 0};
    if (FAILED(transformBuffer->Map(0, &readRange, &mappedData)))
        throw std::runtime_error("DayX_ReadFBX: Failed to map the transform constant buffer.");

    std::memcpy(mappedData, &constants, sizeof(constants));
    transformBuffer->Unmap(0, nullptr);
    return transformBuffer;
}

TransformConstants createTransformConstants()
{
    using namespace DirectX;

    const XMMATRIX world = XMMatrixIdentity();
    const XMVECTOR eyePosition = XMVectorSet(0.0f, 2.25f, -7.5f, 1.0f);
    const XMVECTOR focusPosition = XMVectorZero();
    const XMVECTOR upDirection = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
    const XMMATRIX view = XMMatrixLookAtLH(eyePosition, focusPosition, upDirection);
    const XMMATRIX projection =
        XMMatrixPerspectiveFovLH(XMConvertToRadians(60.0f),
                                 static_cast<float>(FRAME_BUFFER_W) / static_cast<float>(FRAME_BUFFER_H), 0.1f, 100.0f);

    TransformConstants constants{};
    XMStoreFloat4x4(&constants.worldViewProjection, world * view * projection);
    return constants;
}
} // namespace

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
    try
    {
        initWindow(hInstance, hPrevInstance, lpCmdLine, nCmdShow, TEXT("DayX Read FBX"));
        if (g_hWnd == nullptr)
            throw std::runtime_error("DayX_ReadFBX: Failed to create the application window.");

        GraphicsEngine graphicsEngine;
        graphicsEngine.init(g_hWnd, FRAME_BUFFER_W, FRAME_BUFFER_H);

        ID3D12Device* device = graphicsEngine.getD3DDevice();
        const ComPtr<ID3D12RootSignature> rootSignature = createRootSignature(device);

        // 載入用來繪製 FBX Mesh 的 Vertex Shader 與 Pixel Shader
        Shader vertexShader;
        Shader pixelShader;
        vertexShader.loadVS("assets/shaders/fbx.hlsl", "VSMain");
        pixelShader.loadPS("assets/shaders/fbx.hlsl", "PSMain");

        // 建立符合 FBX 頂點格式的 Pipeline State
        PipelineState pipelineState = createPipelineState(rootSignature.Get(), vertexShader, pixelShader);

        const TransformConstants transformConstants = createTransformConstants();
        const ComPtr<ID3D12Resource> transformBuffer = createTransformBuffer(device, transformConstants);

        // 建立用來保存 FBX 模型資料的 Mesh
        Mesh cube;

        // 組合 Cube.fbx 的完整檔案路徑
        const std::filesystem::path cubePath = getExecutableDirectory() / "assets" / "fbx" / "Cube.fbx";

        // 讀取 FBX 並建立對應的 Vertex Buffer 與 Index Buffer
        cube.initFromFbxFile(cubePath.string());

        RenderContext& renderContext = graphicsEngine.getRenderContext();
        while (dispatchWindowMessage())
        {
            graphicsEngine.beginRender();
            renderContext.setRootSignature(rootSignature.Get());

            // 套用能處理 FBX 頂點格式的 Pipeline State
            renderContext.setPipelineState(pipelineState);

            // FBX 載入時已三角化,因此以 Triangle List 方式繪製
            renderContext.setPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
            renderContext.setGraphicsRootConstantBufferView(0, transformBuffer->GetGPUVirtualAddress());

            // 繪製從 Cube.fbx 載入的所有 Mesh
            cube.draw(renderContext);

            graphicsEngine.endRender();
        }

        return EXIT_SUCCESS;
    }
    catch (const std::exception& exception)
    {
        MessageBoxA(nullptr, exception.what(), "DayX_ReadFBX initialization failed", MB_OK | MB_ICONERROR);
        return EXIT_FAILURE;
    }
    catch (...)
    {
        MessageBoxA(nullptr, "An unknown fatal error occurred.", "DayX_ReadFBX initialization failed",
                    MB_OK | MB_ICONERROR);
        return EXIT_FAILURE;
    }
}

結果

參考資料

https://github.com/assimp/assimp


上一篇
Day 21:體積雲實作 1 - Noise、FBM
下一篇
Day 23:體積雲實作 2 - PBR
系列文
因為 AI 看不懂老舊程式,只好乖乖從零開始學 DirectX 12 與 HLSL23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言